package pl.edu.mimuw.irs.core.model.design.net;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import pl.edu.mimuw.irs.core.global.IDManager;
import pl.edu.mimuw.irs.core.model.design.net.IRSNode.NodeType;
import pl.edu.mimuw.irs.core.model.petrinet.Arc;
import pl.edu.mimuw.irs.core.model.petrinet.Net;
import pl.edu.mimuw.irs.core.util.Converter;
import pl.edu.mimuw.irs.core.util.Position;

public abstract class AbstractIRSNodeCopier implements IRSNodeCopier {
	
	// *** Attributes

	protected IRSNode node;
	protected IDManager idManager;
	protected String id;
	protected String name;
	protected NodeType nodeType;
	protected Position position;
	protected Position customPosition;
	protected Fragmentation fragmentation;
	protected Net net;
	protected Collection<Arc> incomingArcs;
	protected Collection<Arc> outgoingArcs;
	public IRSNode getNode() {
		return node;
	}
	public IDManager getIdManager() {
		return idManager;
	}
	public String getId() {
		return id;
	}
	public String getName() {
		return name;
	}
	public NodeType getNodeType() {
		return nodeType;
	}
	public Position getPosition() {
		return position;
	}
	public Position getCustomPosition() {
		return customPosition;
	}
	public Fragmentation getFragmentation() {
		return fragmentation;
	}
	public Net getNet() {
		return net;
	}
	public Collection<Arc> getIncomingArcs() {
		return incomingArcs;
	}
	public Collection<Arc> getOutgoingArcs() {
		return outgoingArcs;
	}
	
	// *** Constructors
	
	public AbstractIRSNodeCopier(IRSNode node) {
		this.node = node;
		this.idManager = node.getIdManager();
		this.id = node.getId();
		this.name = node.getName();
		this.nodeType = node.getNodeType();
		this.position = node.getPosition();
		this.customPosition = node.getCustomPosition();
		this.fragmentation = node.getFragmentation();
		this.net = node.getNet();
		this.incomingArcs = node.getIncomingArcs();
		this.outgoingArcs = node.getOutgoingArcs();
	}
	
	// *** Methods
	
	public IRSNode copy() {
		return copy(CLONE_INCOMING_ARCS | CLONE_OUTGOING_ARCS);
	}

	public IRSNode copy(int options) {
		Map<IRSNode, IRSNode> clonedMap = new LinkedHashMap<IRSNode, IRSNode>();
		IRSNode clone = copy(clonedMap, (SHALLOW & options) == 0);
		if ((CLONE_INCOMING_ARCS & options) != 0) cloneIncomingArcs(getNode(), clone, clonedMap);
		if ((CLONE_OUTGOING_ARCS & options) != 0) cloneOutgoingArcs(getNode(), clone, clonedMap);
		if ((OMIT_NET_ASSIGNMENT & options) == 0) assignNet(clone);
		return clone;
	}
	
	public IRSNode copy(Map<IRSNode, IRSNode> clonesMap, boolean deep) {
		IRSNode clone = createPlainCopy(clonesMap);
		if (deep) copyContents(getNode(), clone, clonesMap);
		return clone;
	}
	
	protected abstract IRSNode createPlainCopy(Map<IRSNode, IRSNode> clonesMap);
	
	protected final void assignNet(IRSNode clone) {
		if (getNet() != null) {
			if (clone.getNet() != null) {
				removeChildrenFromNet(clone.getNet(), clone);
				clone.getNet().removeNode(clone);
			}
			addChildrenToNet(getNet(), clone);
			getNet().addNode(clone);
		}
	}
	
	private void addChildrenToNet(Net net, IRSNode node) {
		for (IRSNode child : node.getChildren()) {
			net.addNode(child);
			addChildrenToNet(net, child);
		}
	}

	private void removeChildrenFromNet(Net net, IRSNode node) {
		for (IRSNode child : node.getChildren()) {
			net.removeNode(child);
			removeChildrenFromNet(net, child);
		}
	}
	
	protected final IRSNode copyContents(IRSNode original, IRSNode clone, Map<IRSNode, IRSNode> clonesMap) {
		List<IRSNode> children = original.getChildren();
		List<IRSNode> clonedChildren = new LinkedList<IRSNode>();
		clonesMap.put(original, clone);
		
		for (IRSNode child : children) {
			IRSNodeCopier copier = child.getCopier();
			IRSNode clonedChild = copier.copy(clonesMap, true);
			clonedChildren.add(clonedChild);
			for (Arc arc : child.getIncomingArcs()) {
				IRSNode irsNode = Converter.asIRSNode(arc.getSource());
				if (clonesMap.containsKey(irsNode)) {
					IRSArc irsArc = Converter.asIRSArc(arc);
					irsArc.copy(clonesMap.get(irsNode), clonedChild);
				}
			}
			for (Arc arc : child.getOutgoingArcs()) {
				/* We do not need to worry about double arcs, because node is connected
				 * with another only if both exists. And this happens only when the second
				 * one is being created. */
				IRSNode irsNode = Converter.asIRSNode(arc.getTarget());
				if (clonesMap.containsKey(irsNode)) {
					((IRSArc)arc).copy(clonedChild, clonesMap.get(irsNode));
				}
			}
		}
		
		clone.setChildren(clonedChildren, getFragmentation());
		clone.setCustomPosition(getCustomPosition());
		
		return clone;
	}
	
	protected final void cloneIncomingArcs(IRSNode original, IRSNode clone, Map<IRSNode, IRSNode> clonesMap) {
		List<IRSNode> originals = original.getInputDescendants();
		originals.add(original);
		
		for (IRSNode node : originals) {
			for (Arc arc : node.getIncomingArcs()) {
				IRSArc irsArc = Converter.asIRSArc(arc);
				/* We omit inner arcs, because those have already been added */
				if (clonesMap.containsKey(irsArc.getSource())) continue;
				/* We must make sure that destination has been cloned */
				if (!clonesMap.containsKey(irsArc.getTarget())) continue;
				/* Here we know this is an outer arc, so we can copy it to cloned node */
				IRSNode clonedTarget = clonesMap.get(irsArc.getTarget());
				irsArc.copy((IRSNode) irsArc.getSource(), clonedTarget);
			}
		}
	}

	protected final void cloneOutgoingArcs(IRSNode original, IRSNode clone, Map<IRSNode, IRSNode> clonesMap) {
		List<IRSNode> originals = original.getOutputDescendants();
		originals.add(original);
		
		for (IRSNode node : originals) {
			for (Arc arc : node.getOutgoingArcs()) {
				IRSArc irsArc = Converter.asIRSArc(arc);
				/* We omit inner arcs, because those have already been added */
				if (clonesMap.containsKey(irsArc.getTarget())) continue;
				/* We must make sure that destination has been cloned */
				if (!clonesMap.containsKey(irsArc.getSource())) continue;
				/* Here we know this is an outer arc, so we can copy it to cloned node */
				IRSNode clonedSource = clonesMap.get(irsArc.getSource());
				irsArc.copy(clonedSource, (IRSNode) irsArc.getTarget());
			}
		}
	}
	
	public void copyTo(IRSNode target) {
		/*if (!getNode().getClass().isInstance(target)) 
			throw new InternalDesignException(DesignCause.INCOMPATIBLE_COPY_NODES, getNode(), target);*/
		copyAttributesTo(target);
		IRSNode copy = copy(OMIT_NET_ASSIGNMENT);
		target.setChildren(new ArrayList<IRSNode>(copy.getChildren()), copy.getFragmentation());
		
		/* 
		 * Node rewritten but children are not connected - we have to propagate incoming/outgoing arcs
		 * to descendants.
		 */
		for (Arc arc : target.getIncomingArcs()) {
			IRSArc irsArc = Converter.asIRSArc(arc);
			/* We want our children to be connected - we are the target of propagation */
			targetPropagate(irsArc);
		}
		for (Arc arc : target.getOutgoingArcs()) {
			IRSArc irsArc = Converter.asIRSArc(arc);
			/* We want our children to be connected - we are the source of propagation */
			sourcePropagate(irsArc);
		}
	}
	
	public final void propagate(IRSArc arc) {
		IRSNode source = (IRSNode) arc.getSource();
		IRSNode target = (IRSNode) arc.getTarget();
		List<IRSNode> sources = source.getOutputDescendants();
		List<IRSNode> targets = target.getInputDescendants();
		sources.add(source);
		targets.add(target);
		
		for (IRSNode sourceNode : sources) {
			for (IRSNode targetNode : targets) {
				/* Omit nodes that are already connected */
				if (sourceNode == source && targetNode == target) continue;
				/* Create connection between these two nodes */
				arc.copy(sourceNode, targetNode);
			}
		}
	}
	
	protected final void targetPropagate(IRSArc arc) {
		IRSNode source = (IRSNode) arc.getSource();
		IRSNode target = (IRSNode) arc.getTarget();
		List<IRSNode> targets = target.getInputDescendants();
		
		for (IRSNode targetNode : targets) {
			/* Create connection between these two nodes */
			arc.copy(source, targetNode);
		}
	}

	protected final void sourcePropagate(IRSArc arc) {
		IRSNode target = (IRSNode) arc.getTarget();
		IRSNode source = (IRSNode) arc.getSource();
		List<IRSNode> sources = source.getOutputDescendants();
		
		for (IRSNode sourceNode : sources) {
			/* Create connection between these two nodes */
			arc.copy(sourceNode, target);
		}
	}
}
